/**
 * @author Florian Leibert
 * 31.10.2006
 **/

package com.headcaselabs.s3;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;

import com.amazon.s3.ListAllMyBucketsResponse;
import com.amazon.s3.ListBucketResponse;
import com.amazon.s3.S3StreamObject;
import com.amazon.s3.Utils;

/**
 * A class of helper methods for implementing streaming in
 * 
 * @author Florian Leibert
 * 
 */
public class ConnectionUtilities {

	private String	server;

	private int			port;

	private String	awsAccessKeyId;

	private String	awsSecretAccessKey;

	/**
	 * Default constructor
	 * 
	 * @param awsAccessKeyId
	 * @param awsSecretAccessKey
	 */
	public ConnectionUtilities(String awsAccessKeyId, String awsSecretAccessKey) {
		this.awsAccessKeyId = awsAccessKeyId;
		this.awsSecretAccessKey = awsSecretAccessKey;
		this.server = com.amazon.s3.Utils.DEFAULT_HOST;
		this.port = Utils.SECURE_PORT;
	}


  /**
   * Add the appropriate Authorization header to the HttpURLConnection.
   * 
   * @param connection
   *            The HttpURLConnection to which the header will be added.
   * @param method
   *            The HTTP method to use (GET, PUT, DELETE)
   * @param resource
   *            The resource name (bucketName + "/" + key).
   */
  private void addAuthHeader(HttpMethod connection, String method, String resource) {
      if (connection.getRequestHeader("Date") == null) {
          connection.setRequestHeader("Date", " " + Utils.httpDate());
      }
      if (connection.getRequestHeader("Content-Type") == null) {
          connection.setRequestHeader("Content-Type", "");
      }

      String canonicalString = Utils.makeCanonicalString(method, resource, connection.getRequestHeaders());
      String encodedCanonical = Utils.encode(this.awsSecretAccessKey, canonicalString, false);
      connection.setRequestHeader("Authorization", " AWS " + this.awsAccessKeyId + ":" + encodedCanonical);
  }

	/**
	 * Create a new URL object for a given resource.
	 * 
	 * @param resource
	 *          The resource name (bucketName + "/" + key).
	 */
	private URL makeURL(String resource) throws MalformedURLException {
		String protocol = "https";
		return new URL(protocol, this.server, this.port, "/" + resource);
	}

	/**
	 * Add the given headers to the HttpURLConnection with a prefix before the
	 * keys.
	 * 
	 * @param connection
	 *          The HttpURLConnection to which the headers will be added.
	 * @param headers
	 *          A Map of String to List of Strings representing the http headers
	 *          to pass (can be null).
	 * @param prefix
	 *          The string to prepend to each key before adding it to the
	 *          connection.
	 */
	private void addHeaders(HttpMethod connection, Map headers, String prefix) {
		if (headers != null) {
			for (Iterator i = headers.keySet().iterator(); i.hasNext();) {
				String key = (String) i.next();
				for (Iterator j = ((List) headers.get(key)).iterator(); j.hasNext();) {
					String value = (String) j.next();
					connection.addRequestHeader(prefix + key, value);
				}
			}
		}
	}

	/**
	 * @return
	 */
	public HttpClient getHttpClient() {
		HttpConnectionManagerParams connectionParams = new HttpConnectionManagerParams();
		connectionParams.setConnectionTimeout(6600000);
		connectionParams.setSoTimeout(6600000);
		connectionParams.setStaleCheckingEnabled(true);
		connectionParams.setSendBufferSize(4000);
		MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
		connectionManager.setParams(connectionParams);
		HttpClientParams clientParams = new HttpClientParams();
		clientParams.setSoTimeout(6600000);
		HttpClient client = new HttpClient(clientParams, connectionManager);
		return client;
	}

	/**
	 * Retrieves a Bucket
	 * 
	 * @param bucket
	 * @return the response code
	 * @throws MalformedURLException
	 * @throws Exception
	 */
	public int getBucket(String bucket) throws MalformedURLException, Exception {

		String path = Utils.pathForListOptions(bucket, null, null, null);
		GetMethod get = (GetMethod) makeRequest("GET", path, null, null);
		HttpClient client = getHttpClient();
		int response = client.executeMethod(get);
		return response;

	}

	/**
	 * Creates a new bucket
	 * 
	 * @param bucket
	 * @return the ResponseCode
	 * @throws MalformedURLException
	 * @throws Exception
	 */
	public int createBucket(String bucket) throws MalformedURLException,
			Exception {
		PutMethod get = (PutMethod) makeRequest("PUT", bucket, null, null);
		HttpClient client = getHttpClient();
		int response = client.executeMethod(get);
		return response;

	}

	/**
	 * Writes an Object as a stream to S3
	 */
	public int putStream(String bucket, String key, S3StreamObject object,
			File file, Map headers) throws MalformedURLException, Exception {
		int response;
		long contentLength = S3Utils.addMeta(new FileInputStream(file), object);
		RequestEntity re = new InputStreamRequestEntity(new FileInputStream(file),
				contentLength, "application/octet-stream");
		PutMethod put = (PutMethod) makeRequest("PUT", bucket + "/"
				+ Utils.urlencode(key), headers, object);
		put.setRequestEntity(re);
		Header[] th = put.getRequestHeaders();
		for (Header h : th) {
			if (h.getName().equals("null") || h.getValue().equals("")) {
				put.removeRequestHeader(h);
			}
			// System.out.println(h.getName() + ":" + h.getValue());
		}
		HttpClient client = getHttpClient();
		try {
			response = client.executeMethod(put);
			// System.out.println(put.getResponseBodyAsString());
		} finally {
			put.releaseConnection();
		}
		return response;
	}

	/**
   * Reads an object from S3.
   * @param bucket The name of the bucket where the object lives.
   * @param key The name of the key to use.
   * @param headers A Map of String to List of Strings representing the http
   * headers to pass (can be null).
   */
  public GetMethod get(String bucket, String key, Map headers)
      throws Exception
  {
      GetMethod gr = (GetMethod)makeRequest("GET", bucket + "/" + Utils.urlencode(key), headers,null);
      gr.setDoAuthentication(true);
      HttpClient client = getHttpClient();
      client.executeMethod(gr);
      return gr;
  }
  
  

	/**
	 * Writes an Object as a stream to S3
	 */
	public int putStream(String bucket, String key, S3StreamObject object,
			long contentLength, String encoding, InputStream is, Map headers)
			throws MalformedURLException, Exception {
		int response;
		// long contentLength = com.headcaselabs.backup.s3.Utils.addMeta(is,
		// object);
		RequestEntity re = new InputStreamRequestEntity(is, contentLength, encoding);
		PutMethod put = (PutMethod) makeRequest("PUT", bucket + "/"
				+ Utils.urlencode(key), headers, object);
		put.setRequestEntity(re);
		Header[] th = put.getRequestHeaders();
		for (Header h : th) {
			if (h.getName().equals("null") || h.getValue().equals("")) {
				put.removeRequestHeader(h);
			}
		}
		HttpClient client = getHttpClient();
		try {
			response = client.executeMethod(put);
		} finally {
			put.releaseConnection();
		}
		return response;
	}

	/**
	 * Make a new HttpURLConnection.
	 * 
	 * @param method
	 *          The HTTP method to use (GET, PUT, DELETE)
	 * @param resource
	 *          The resource name (bucketName + "/" + key).
	 * @param headers
	 *          A Map of String to List of Strings representing the http headers
	 *          to pass (can be null).
	 * @param object
	 *          The S3Object that is to be written (can be null).
	 */
  public HttpMethod makeRequest(String method, String resource, Map headers, S3StreamObject object) throws Exception {
    URL url = makeURL(resource);
    // System.out.println("URL:" + url.toURI().toString());
    HttpMethod httpMethod = null;
    if (method.equals("PUT"))
        httpMethod = new PutMethod(url.toURI().toString());
    else if (method.equals("GET"))
        httpMethod = new GetMethod(url.toURI().toString());
    else if (method.equals("DELETE")) 
        httpMethod = new DeleteMethod(url.toURI().toString());
    addHeaders(httpMethod, headers, "");

    if (object != null && object.metadata != null && object.metadata.size() > 0)
        addHeaders(httpMethod, object.metadata, Utils.METADATA_PREFIX);
    addAuthHeader(httpMethod, method, resource);
    return httpMethod;
}	
	 /**
   * Attempts to delete a bucket
   * @param bucket
   * @param headers
   * @return
   * @throws Exception
   */
  public HttpMethod deleteBucket(String bucket, Map headers) throws Exception {
      HttpMethod hm =  makeRequest("DELETE",bucket, headers, null);
      HttpClient client = getHttpClient();
      //System.out.println(client.getState());
      try {
          client.executeMethod(hm);
          //System.out.println(hm.getResponseBodyAsString());
      } finally {
          hm.releaseConnection();
      }
      return hm;
  }
  
  public HttpMethod deleteObject(String bucket, String object) throws Exception {
      HttpMethod hm =  makeRequest("DELETE",bucket + "/" + Utils.urlencode(object), null, null);
      HttpClient client = getHttpClient();
      //System.out.println(client.getState());
      try {
          client.executeMethod(hm);
          //System.out.println(hm.getResponseBodyAsString());
      } finally {
          hm.releaseConnection();
      }
      return hm;
  }

  /**
   * Lists the contents of a bucket
   * @param bucket
   * @param prefix
   * @param marker
   * @param maxKeys
   * @param delimiter
   * @param headers
   * @return
   * @throws Exception
   */
  public ListBucketResponse listBucket(String bucket, String prefix, String marker, Integer maxKeys,
          String delimiter, Map headers) throws Exception {


      String path = Utils.pathForListOptions(bucket, prefix, marker, maxKeys, delimiter);
      HttpMethod hm = makeRequest("GET", path, headers,null);
      ListBucketResponse lbr = null;
      HttpClient client = getHttpClient();
      try {
          client.executeMethod(hm);
          lbr = new ListBucketResponse(hm);
          // System.out.println(put.getResponseBodyAsString());
      } finally {
          hm.releaseConnection();
      }
      return lbr;
  }
  
  
  
  /**
   * Lists all buckets
   * @param headers
   * @return
   * @throws Exception
   */
  public ListAllMyBucketsResponse listAllMyBuckets(Map headers)
      throws Exception
  {
      URL url = new URL("https", this.server, this.port, "/" );
      HttpMethod hm = new GetMethod(url.toURI().toString());
      addAuthHeader(hm, "GET", "");
      HttpClient client = getHttpClient();
      ListAllMyBucketsResponse lbr = null;
      try {
          client.executeMethod(hm);
          lbr = new ListAllMyBucketsResponse(hm);
      } finally {
          hm.releaseConnection();
      }
      return lbr;
  }

}
